/*
 * Created on 2006-01-07
 */
package dk.aiszone.lang.comparatorfactory;

import java.io.IOException;
import java.io.StringReader;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.codehaus.janino.ByteArrayClassLoader;
import org.codehaus.janino.ClassLoaderIClassLoader;
import org.codehaus.janino.CompileException;
import org.codehaus.janino.DebuggingInformation;
import org.codehaus.janino.Java;
import org.codehaus.janino.Parser;
import org.codehaus.janino.Scanner;
import org.codehaus.janino.UnitCompiler;
import org.codehaus.janino.Parser.ParseException;
import org.codehaus.janino.Scanner.ScanException;
import org.codehaus.janino.util.ClassFile;
import org.jdom.Element;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

import dk.aiszone.lang.autosource.formatter.JavaFormatterImpl;
import dk.aiszone.lang.autosource.generatorobjects.Argument;
import dk.aiszone.lang.autosource.generatorobjects.Class;
import dk.aiszone.lang.autosource.generatorobjects.ClassBody;
import dk.aiszone.lang.autosource.generatorobjects.Declaration;
import dk.aiszone.lang.autosource.generatorobjects.JavaDoc;
import dk.aiszone.lang.autosource.generatorobjects.JavaFile;
import dk.aiszone.lang.autosource.generatorobjects.Method;
import dk.aiszone.lang.autosource.generatorobjects.Scope;
import dk.aiszone.lang.autosource.generatorobjects.Variable;

/**
 * The <code>ComparatorFactory</code> is used for creating <code>Comparator</code> classes based on XML data.<br>
 * 
 * <pre>
 *      &lt;comparator name="test"&gt;
 *          &lt;comparator-object id="medication" class="dk.acure.epm.model.intervention.Medication" /&gt;
 *          &lt;comparator-object id="drug" class="dk.acure.epm.model.drugs.Drug" /&gt;
 * 
 *          &lt;comparator-definition name="one"&gt;
 *              &lt;comparator-item object="medication" attribute="MedicationType" descending="true" /&gt;
 *              &lt;comparator-item object="drug" attribute="DrugName" /&gt;
 *              &lt;comparator-item object="medication"&gt;
 *                  &lt;comparator-join class="dk.acure.epm.model.AbsoluteDateTime&gt;
 *                      &lt;comparator-attribute name="StartDay" /&gt;
 *                      &lt;comparator-attribute name="StartTime" /&gt;
 *                      &lt;comparator-method name="createAbsoluteDateTime" /&gt;
 *                  &lt;/comparator-join&gt;
 *             &lt;/comparator-item&gt;
 *          &lt;/comparator-definition&gt;
 * 
 *      &lt;/comparator&gt;
 *      
 *      &lt;comparator name="test"&gt;
 *          &lt;comparator-object id="medication" class="dk.acure.epm.model.intervention.Medication" /&gt;
 * 
 *          &lt;comparator-definition name="two"&gt;
 *              &lt;comparator-item object="medication" attribute="ContactPK" /&gt;
 *              &lt;comparator-item object="medication"&gt;
 *                  &lt;!-- how to sort according to objects in collections --&gt;
 *              &lt;/comparator-item&gt;
 *          &lt;/comparator-definition&gt;
 * 
 *      &lt;/comparator&gt;
 *
 * </pre>
 * 
 * @author <a href="mailto:ais@mail.dk">Ais</a>
 */
public class ComparatorFactory {

    private static final String COMPARATOR = "comparator";

    private static final String COMPARATOR_OBJECT = "comparator-object";

    private static final String COMPARATOR_DEFINITION = "comparator-definition";

    private static final String COMPARATOR_ITEM = "comparator-item";

    private static final String COMPARATOR_JOIN = "comparator-join";

    private static final String COMPARATOR_ATTRIBUTE = "comparator-attribute";

    private static final String COMPARATOR_METHOD = "comparator-method";

    private JavaFile file;

    /**
     * Constructor for the <code>ComparatorFactory</code>.<br>
     * 
     * @param xml
     * @throws IllegalArgumentException
     * @throws ComparatorFactoryException
     */
    public ComparatorFactory(Element xml) throws IllegalArgumentException, ComparatorFactoryException {
        if (xml == null) {
            throw new IllegalArgumentException("Supplied org.jdom.Element was null");
        }

        if (!COMPARATOR.equals(xml.getName())) {
            throw new ComparatorFactoryException("Illegal name of root element (" + xml.getName() + ")");
        }

        String className = xml.getAttributeValue("name");
        ClassBody body = new ClassBody(new Declaration(Scope.PUBLIC), className);
        body.addImplements(new Class("java.util.Comparator"));

        Map<String, String> name2classes = extractVariables(xml, body);
        Method compareMethod = createComparatorMethod(xml.getChild(COMPARATOR_DEFINITION), name2classes);
        body.addMethod(compareMethod);

        Method toString = new Method(new Declaration(Scope.PUBLIC), new Class("String"), "toString");
        toString.appendToBody("return \"" + new JavaFormatterImpl().toJava(compareMethod, 0).replaceAll("\\\"", "\\\\\"").replaceAll("\\\n", "\\\\n") + "\";");

        JavaDoc javaDocToString = new JavaDoc();
        javaDocToString.setDescription("Returns a <code>String</code> containing the implementation of the <code>compareTo(Object, Object)</code> method.");
        javaDocToString.setReturn("<code>String</code> representation of the comparator implementation.");
        toString.setJavaDoc(javaDocToString);

        body.addMethod(toString);

        Format xmlFormat = Format.getPrettyFormat();
        xmlFormat.setEncoding("ISO-8859-1");

        Method toXML = new Method(new Declaration(Scope.PUBLIC), new Class("String"), "toXMLString");
        toXML.appendToBody("return \"" + new XMLOutputter(xmlFormat).outputString(xml).replaceAll("\\\"", "\\\\\"").replaceAll("\\\n", "\\\\n").replaceAll("\\r", "") + "\";");

        JavaDoc javaDocToXML = new JavaDoc();
        javaDocToXML.setDescription("Returns a <code>String</code> containing the XML definition which lay basis for the <code>Comparator</code>.");
        javaDocToXML.setReturn("<code>String</code> representation of the comparator definition.");
        toXML.setJavaDoc(javaDocToXML);

        body.addMethod(toXML);

        Class opClass = new Class("dk.aiszone.lang.comparatorfactory.ComparatorObjectProvider");

        Method constructor = new Method();
        constructor.setDeclaration(new Declaration(Scope.PUBLIC));
        constructor.setReturnType(Class.CONSTRUCTOR);
        constructor.addArgument(new Argument(opClass, "objectProvider"));
        constructor.appendToBody("this.objectProvider = objectProvider;");
        body.addConstructor(constructor);

        Variable variable = new Variable();
        variable.setName("objectProvider");
        variable.setDeclaration(new Declaration(Scope.PRIVATE));
        variable.setType(opClass);
        body.addVariable(variable);

        JavaDoc javadoc = new JavaDoc();
        javadoc.setDescription("Implementation of the " + className + " <code>Comparator</code>.");
        body.setJavaDoc(javadoc);

        file = new JavaFile("dk.aiszone.lang.comparators", body);
    }

    @SuppressWarnings("unchecked")
    public java.lang.Class<Comparator<?>> compileClass() throws ComparatorFactoryException {
        try {
            String body = new NoImportJavaFormatter().toJava(file);

            Scanner scanner = new Scanner(null, new StringReader(body));

            Java.CompilationUnit compilationUnit = new Parser(scanner).parseCompilationUnit();

            ClassLoaderIClassLoader classLoaderIClassLoader = new ClassLoaderIClassLoader(Thread.currentThread().getContextClassLoader());

            // Compile compilation unit to class files.
            ClassFile[] classFiles = new UnitCompiler(compilationUnit, classLoaderIClassLoader).compileUnit(DebuggingInformation.DEFAULT_DEBUGGING_INFORMATION);

            // Convert the class files to bytes and store them in a Map.
            Map<String, byte[]> classes = new HashMap<String, byte[]>(); // String className => byte[] data
            for (int i = 0; i < classFiles.length; ++i) {
                ClassFile cf = classFiles[i];
                classes.put(cf.getThisClassName(), cf.toByteArray());
            }

            // Create a ClassLoader that loads the generated classes.
            java.lang.Class _class = new ByteArrayClassLoader(classes, classLoaderIClassLoader.getClassLoader()).loadClass(file.getMainClass().getPackage());

            return _class;
        } catch (CompileException e) {
            throw new ComparatorFactoryException(e);
        } catch (ParseException e) {
            throw new ComparatorFactoryException(e);
        } catch (ScanException e) {
            throw new ComparatorFactoryException(e);
        } catch (ClassNotFoundException e) {
            throw new ComparatorFactoryException(e);
        } catch (IOException e) {
            throw new ComparatorFactoryException(e);
        }
    }

    /**
     * Returns a <code>String</code> containing the generated code based on the supplied XML.
     * 
     * @return Generated code.
     */
    public String dumpToString() {
        return new JavaFormatterImpl().toJava(file);
    }

    private void appendComparison(StringBuffer body, Map<String, String> name2class, Element comparatorItem, Set<String> declaredObjects) {
        String objectName = comparatorItem.getAttributeValue("object");

        if (!declaredObjects.contains(objectName)) {
            declaredObjects.add(objectName);

            String _class = name2class.get(objectName);

            body.append(_class).append(" ").append(objectName).append("1 = (").append(_class).append(")objectProvider.getObject(arg1, \"").append(objectName).append("\");\n");
            body.append(_class).append(" ").append(objectName).append("2 = (").append(_class).append(")objectProvider.getObject(arg2, \"").append(objectName).append("\");\n \n");
        }

        String var1;
        String var2;

        String attribute = comparatorItem.getAttributeValue("attribute");
        if (attribute != null) {
            var1 = objectName + "1_" + attribute;
            var2 = objectName + "2_" + attribute;

            body.append("Comparable ").append(var1).append(" = ").append(objectName).append("1.get").append(attribute).append("();\n");
            body.append("Comparable ").append(var2).append(" = ").append(objectName).append("2.get").append(attribute).append("();\n");
        } else {
            var1 = null;
            var2 = null;
        }

        body.append("if (").append(var1).append(" == null && ").append(var2).append(" == null) {\n");
        body.append("val = 0;\n");
        body.append("} else if (").append(var1).append(" != null && ").append(var2).append(" == null) {\n");
        body.append("val = -1;\n");
        body.append("} else if (").append(var1).append(" == null && ").append(var2).append(" != null) {\n");
        body.append("val = 1;\n");
        body.append("} else {\n");
        body.append("val = ").append(var1).append(".compareTo(").append(var2).append(");\n");
        body.append("}\n\n");
        body.append("if (val != 0) {\n");
        body.append("return val;\n");
        body.append("}\n \n");
    }

    private Method createComparatorMethod(Element xml, Map<String, String> name2class) {
        Method method = new Method();

        method.addArgument(new Argument(new Class("Object"), "arg1"));
        method.addArgument(new Argument(new Class("Object"), "arg2"));
        method.setDeclaration(new Declaration(Scope.PUBLIC));
        method.setReturnType(new Class("int"));
        method.setName("compare");
        method.appendToBody(createMethodBody(xml, name2class));

        return method;
    }

    @SuppressWarnings("unchecked")
    private String createMethodBody(Element xml, Map<String, String> name2class) {
        StringBuffer body = new StringBuffer(100);
        body.append("int val;\n \n");

        Set<String> declaredObjects = new HashSet<String>();

        for (Iterator<Element> iter = xml.getChildren(COMPARATOR_ITEM).iterator(); iter.hasNext();) {
            appendComparison(body, name2class, iter.next(), declaredObjects);
        }

        body.append("return 0;");

        return body.toString();
    }

    @SuppressWarnings("unchecked")
    private Map<String, String> extractVariables(Element xml, ClassBody body) {
        Map<String, String> name2classes = new HashMap<String, String>();

        for (Iterator<Element> iter = xml.getChildren(COMPARATOR_OBJECT).iterator(); iter.hasNext();) {
            Element object = iter.next();

            Class _class = new Class(object.getAttributeValue("class"));

            name2classes.put(object.getAttributeValue("id"), _class.getPackageName());
        }

        return name2classes;
    }

    /**
     * Exception thrown by the <code>ComparatorFactory</code> if something happens during creation of the <code>Comparator</code>.
     */
    public static class ComparatorFactoryException extends Exception {

        private ComparatorFactoryException(String message) {
            super(message);
        }

        private ComparatorFactoryException(Exception e) {
            super(e);
        }
    }
}